﻿using System;
using System.Windows.Threading;
using System.Xml.Linq;
using KnowledgeKiosk.Parsers;

namespace KnowledgeKiosk.FeedUpdaters
{
    /// <summary>
    /// Provides an abstract interface for periodically loading object-data from a XML based source.
    /// </summary>
    /// <typeparam name="T">Specifies the type of objects that will be retrieved.</typeparam>
    /// <remarks>
    /// </remarks>
    public abstract class AbstractXmlFeedUpdater<T>
    {
        /// <summary>
        /// The DispatcherTimer object used to perform periodic updates.
        /// </summary>
        private readonly DispatcherTimer _timer;

        /// <summary>
        /// A hash value of a feed result. This is used to compare two the results of two update cycles and
        /// determine whether the feed has actually updated.
        /// </summary>
        private int _feedHash;

        /// <summary>
        /// The number of times the retry mechanism has attempted to retry a failed update.
        /// </summary>
        private int _retryCount;

        /// <summary>
        /// Indicates whether the retry mechanism is enabled.
        /// </summary>
        private bool _retryEnabled = true;

        /// <summary>
        /// Instantiates a new instance of the AbstractXmlFeedUpdater class using the specified source,
        /// parser and update interval.
        /// </summary>
        /// <param name="feedUri">Uri of the source.</param>
        /// <param name="xmlParser">Parser that translates XML to strongly typed objects.</param>
        /// <param name="updateInterval">Interval of the update cycle.</param>
        protected AbstractXmlFeedUpdater(Uri feedUri, IXmlToObjectParser<T> xmlParser, TimeSpan updateInterval)
        {
            FeedUri = feedUri;
            UpdateInterval = updateInterval;
            XmlParser = xmlParser;

            _timer = new DispatcherTimer();
            _timer.Interval = UpdateInterval;
            _timer.Tick += Tick;
        }

        /// <summary>
        /// Gets the Uri of the source.
        /// </summary>
        public Uri FeedUri { get; private set; }

        /// <summary>
        /// Gets the interval of the update cycle.
        /// </summary>
        public TimeSpan UpdateInterval { get; private set; }

        /// <summary>
        /// Gets the parser that translates XML to strongly typed objects.
        /// </summary>
        public IXmlToObjectParser<T> XmlParser { get; private set; }

        /// <summary>
        /// Gets or sets a value that indicates whether the updater should retry failed updates.
        /// </summary>
        public bool RetryEnabled
        {
            get { return _retryEnabled; }
            set { _retryEnabled = value; }
        }

        /// <summary>
        /// Gets a value that indicates whether the updater is currently retrying.
        /// </summary>
        public bool IsRetrying
        {
            get { return _retryCount > 0; }
        }

        /// <summary>
        /// Occurs when the feed has received new information.
        /// </summary>
        public event EventHandler<EventArgs<T>> Updated;

        /// <summary>
        /// Called when the update interval has elapsed.
        /// </summary>
        internal abstract void Tick(object sender, EventArgs e);

        /// <summary>
        /// Parses the XML data into a strongly-typed object
        /// </summary>
        /// <remarks>
        /// This method will only parse the XML data if it is different from the data of the previous update cycle.
        /// Exceptions thrown during parsing will enable the retry mechanism of the updater. 
        /// </remarks>
        /// <param name="data">The XML data to parse.</param>
        protected void ParseXmlData(XElement data)
        {
            try
            {
                int newFeedHash = data.ToString().GetHashCode();
                if (_feedHash != newFeedHash)
                {
                    _feedHash = newFeedHash;
                    T xml = XmlParser.Parse(data);
                    OnUpdated(new EventArgs<T>(xml));
                    Logger.WriteLine("Updated XML feed: {0}", FeedUri);
                }
                if (RetryEnabled && IsRetrying)
                    ResetRetryInterval();
            }
            catch (Exception ex)
            {
                Logger.WriteLine(ex);

                if (RetryEnabled)
                    SetRetryInterval();
            }
        }

        /// <summary>
        /// Updates the retry interval
        /// </summary>
        protected void SetRetryInterval()
        {
            TimeSpan newInterval = TimeSpan.FromSeconds(Math.Min(Convert.ToInt32(Math.Pow(2, _retryCount + 2)), UpdateInterval.TotalSeconds));
            if (newInterval < UpdateInterval)
                _retryCount++;
            _timer.Interval = newInterval;
            Logger.WriteLine("Failed to download or parse XML feed '{0}' Retrying in {1}...", FeedUri, _timer.Interval);
        }

        /// <summary>
        /// Resets the retry interval, thus stopping the updater from retrying a previously failed update
        /// </summary>
        protected void ResetRetryInterval()
        {
            _retryCount = 0;
            _timer.Interval = UpdateInterval;
        }

        /// <summary>
        /// Raises the Updated event
        /// </summary>
        /// <param name="e">The arguments of the event that was raised. Contains the updated data.</param>
        protected virtual void OnUpdated(EventArgs<T> e)
        {
            EventHandler<EventArgs<T>> handler = Updated;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Starts the updater
        /// </summary>
        public void Start()
        {
            Tick(this, null);
            _timer.Start();
        }

        /// <summary>
        /// Stops the updater
        /// </summary>
        public void Stop()
        {
            _timer.Stop();
        }
    }
}